﻿using System;
using System.Collections.Generic;
using Mesuro.Common;
using System.ComponentModel;

namespace Mesuro.Graphing {
	[Flags]
	public enum VisibleScaleFlags {
		None = 0,
		LockBottom = 1,
		LockTop = 2,
		LockRange = LockBottom | LockTop,
		ShowAllData = 4,
		LockNumTicks = 8
	}
	public class VisibleScale : IScale, INotifyPropertyChanged {
		public const string PROP_Range = "Range";
		public const string PROP_Domain = "Domain";
		public const string PROP_Ticks = "Ticks";
		public const string PROP_VisibleRange = "VisibleRange";
		public const string PROP_Label = "Label";
		public const string PROP_Invert = "Invert";

		/*
		public struct TickDescription {
			public int nTotal, nMajor, firstMajor, majorInterval;
			public decimal firstTick, tickInterval;
			public bool IsMajor( int n ) {
				if (majorInterval == 0) return false;
				return ((n - firstMajor) % majorInterval) == 0;
			}
		}
		public enum TruncationPolicy { FLOOR, CEIL };
		public delegate double TransformHandler( double d );
		public static decimal Nearest( double d, int dp, out int exp10 ) {
			if (d == 0) { exp10 = 1; return 0M; }
			if (Double.IsInfinity( d )) { throw new ArgumentException( "Cannot accurately represent infinite value" ); }
			int sgn = (d < 0) ? -1 : 1;
			d *= sgn;
			exp10 = (int)Math.Less( Math.Log10( d ) );
			double mant = d / Math.Pow( 10, exp10 );
			decimal ret = (decimal)Math.Nearest( mant, dp - 1 );
			return sgn * ret;
		}
		public static decimal Nearest( double d, int dp ) {
			int exp10;
			decimal ret = Nearest( d, dp, out exp10 );
			return ret * (decimal)Math.Pow( 10, exp10 );
		}
		public static TickDescriptor[] SuggestTicks( Limits dataLimits, int n ) {
			// Format inputs
			dataLimits = dataLimits.GetUninverted();
			if (dataLimits.Empty || n == 0) return new TickDescriptor[0];
			n = (n < 0) ? -n : n;
			// Niceify limits
			double a = dataLimits.Min;
			double R = dataLimits.Range;
			decimal mag = (decimal)Math.Pow( 10, Math.Less( Math.Log10( R / n ) ) );
			decimal a2 = mag * (decimal)Math.Less( a / (double)mag );
			decimal end = mag * (decimal)Math.More( dataLimits.Max / (double)mag );
			decimal r = (decimal)Math.Less( ((double)end - (double)a2) / ((double)mag * n) );
			switch ((int)r) {
				case 3:
					r -= 0.5M;
					break;
				case 4:
				case 7:
				case 9:
					r += 1;
					break;
			}
			r *= mag;
			n = (int)Math.More( (dataLimits.Max - (double)a2) / (double)r );
			if (a2 == -end) {
				a2 = -r * n / 2;
			}
			int nTot = (int)(n * r / mag);
			if (nTot == n) {
				nTot *= 5;
				mag /= 5;
			}
			int minorRatio = nTot / n;
			TickDescriptor[] ticks = new TickDescriptor[nTot + 1];
			for (int i = 0; i <= nTot; i++) {
				ticks[i] = new TickDescriptor( (decimal)(a2 + i * mag), (i % minorRatio) == 0 );
			}
			return ticks;
		}
		public static TickDescription CalculateTicks( Limits dataLimits, int n ) {
			// Ensure Limits are not inverted
			dataLimits = dataLimits.GetUninverted();
			//Ensure non-degeneracy
			if (dataLimits.Empty || n == 0) return new TickDescription();
			//Ensure positivity
			n = (n < 0) ? -n : n;
			TickDescription desc = new TickDescription();
			// Niceify limits
			double dataStart = dataLimits.Min;
			double range = dataLimits.Range;
			double mag = Math.Pow( 10, Math.Less( Math.Log10( range / n ) ) );
			double scaleStart = (mag * Math.Less( dataStart / mag ));
			double end = mag * Math.More( dataLimits.Max / mag );
			decimal _end = (decimal)mag * (int)end;
			double interval = Math.Less( (end - scaleStart) / (mag * n) );
			switch ((int)interval) {
				case 3:
					interval -= 0.5;
					break;
				case 4:
				case 7:
				case 9:
					interval += 1;
					break;
			}
			interval *= mag;

			n = (int)Math.More( (dataLimits.Max - scaleStart) / interval );
			desc.nMajor = n;

			if (scaleStart == -end) {
				scaleStart = -interval * n / 2;
			}
			desc.firstTick = (decimal)scaleStart;

			int nTot = (int)(n * interval / mag);
			if (nTot == n) {
				nTot *= 5;
				mag /= 5;
			}
			desc.nTotal = nTot;

			desc.majorInterval = nTot / n;

			desc.tickInterval = (nTot == 0) ? 0 : (decimal)(mag * Math.Nearest( (end - scaleStart) / (mag * nTot) ));

			//int minorRatio = nTot / rhoVal;
			return desc;
		}
		*/
		public enum TruncationMode { Less, More, ToZero, FromZero, Nearest }
		public static decimal Niceify( double val, int dp, TruncationMode truncation = TruncationMode.Nearest ) {
			if (val == 0) {
				return 0M;
			}
			try {
				double mag = Math.Pow( 10, dp );
				double ret;
				switch (truncation) {
					case TruncationMode.Less:
						ret = Math.Floor( val * mag ); break;
					case TruncationMode.More:
						ret = Math.Ceiling( val * mag ); break;
					case TruncationMode.ToZero:
						ret = (int)(val * mag); break;
					case TruncationMode.FromZero:
						ret = val * mag;
						ret = (ret == (int)ret) ? ret : 1 + (int)ret;
						break;
					case TruncationMode.Nearest:
					default:
						ret = Math.Round( val * mag ); break;
				}
				return (decimal)ret / (decimal)mag;
			} catch {
				return 0;
			}
		}
		public static decimal Niceify( double val, int sigFigs, out double magnitude, TruncationMode truncation = TruncationMode.Nearest ) {
			if (val == 0) {
				magnitude = 1;
				return 0M;
			}
			magnitude = (int)Math.Floor( Math.Log10( val ) ) - sigFigs;
			double mag = Math.Pow( 10, magnitude );
			switch (truncation) {
				case TruncationMode.Less:
					val = Math.Floor( val / mag ); break;
				case TruncationMode.More:
					val = Math.Ceiling( val / mag ); break;
				case TruncationMode.ToZero:
					val = (int)(val / mag); break;
				case TruncationMode.FromZero:
					val = val / mag;
					val = (val == (int)val) ? val : 1 + (int)val;
					break;
				case TruncationMode.Nearest:
				default:
					val = Math.Round( val / mag ); break;
			}
			switch ((int)val % 10) {
				case 1: val -= 1; break;
				case 3: val += 2; break;
				case 6: val -= 1; break;
				case 7: val -= 2; break;
				case 8: val += 2; break;
				case 9: val += 1; break;
			}
			return (decimal)val;
		}

		private IScale _scale;
		private int _numTicks = 5;
		private int _numTicksDesired = 5;
		private VisibleScaleFlags _flags = VisibleScaleFlags.ShowAllData;
		private int _significance = 1;
		private int _tickSig = 0;

		public event PropertyChangedEventHandler PropertyChanged;
		private void RaisePropertyChanged( string prop ) {
			if (PropertyChanged != null)
				PropertyChanged( this, new PropertyChangedEventArgs( prop ) );
		}

		public IScale Scale {
			get { return _scale; }
			set {
				if (_scale == value) return;
				IScale old = _scale;
				_scale = value;
				RaisePropertyChanged( PROP_Domain );
				RaisePropertyChanged( PROP_VisibleRange );
			}
		}
		public int NumTicks {
			get { return _numTicks; }
			set { _numTicksDesired = Math.Max( 2, value ); PrepTicks(); }
		}
		public VisibleScaleFlags Flags {
			get { return _flags; }
			set { _flags = value; }
		}
		public int Significance {
			get { return _significance; }
			set { _significance = value; }
		}

		private bool _invert;
		public bool Invert {
			get { return _invert; }
			set {
				_invert = value;
				RaisePropertyChanged( PROP_Invert );
			}
		}
		private Limits _range = new Limits();
		/// <summary>
		/// Gets/sets the limits of this scale in _data-space
		/// </summary>
		public Limits Range {
			get { return _range; }
			set {
				_range = value;
				RaisePropertyChanged( PROP_Range );
				PrepTicks();
			}
		}

		/// <summary>
		/// Gets/sets the visible limits of this scale in _data-space
		/// </summary>
		public Limits VisibleRange {
			get { return Scale.Range; }
			private set { Scale.Range = value; RaisePropertyChanged( PROP_VisibleRange ); }
		}

		/// <summary>
		/// Gets/sets the limits of this scale in basedOn-space
		/// </summary>
		public Limits Domain {
			get { return Scale.Domain; }
			set { Scale.Domain = value; RaisePropertyChanged( PROP_Domain ); PrepTicks(); }
		}

		public VisibleScale() : this( new LinearScale() ) { }
		public VisibleScale( IScale underlyingScale ) {
			Scale = underlyingScale;
		}

		public double ToDataSpace( double d ) {
			return Scale.ToDataSpace( Invert ? Domain.Max + Domain.Min - d : d );
		}

		public double ToPlotSpace( double d ) {
			double p = Scale.ToPlotSpace( d );
			return Invert ? Domain.Max + Domain.Min - p : p;
		}

		public void PrepTicks() {
			//Decode lock flags
			bool lockMin = Flags.HasFlag( VisibleScaleFlags.LockBottom );
			bool lockMax = Flags.HasFlag( VisibleScaleFlags.LockTop );
			bool lockNum = Flags.HasFlag( VisibleScaleFlags.LockNumTicks );

			//Decode truncation modes
			TruncationMode minRound = Flags.HasFlag( VisibleScaleFlags.ShowAllData ) ? (Domain.Inverted ? TruncationMode.More : TruncationMode.Less) : TruncationMode.Nearest;
			TruncationMode maxRound = Flags.HasFlag( VisibleScaleFlags.ShowAllData ) ? (Domain.Inverted ? TruncationMode.Less : TruncationMode.More) : TruncationMode.Nearest;

			int Mr = (int)(Math.Log10( Range.Range ) + .1);
			_tickSig = Significance - Mr;

			Limits vrange = new Limits();
			vrange.Min = (lockMin) ? Range.Min : (double)Niceify( Range.Min, _tickSig, minRound );
			vrange.Max = (lockMax) ? Range.Max : (double)Niceify( Range.Max, _tickSig, maxRound );
			if (vrange.Min != VisibleRange.Min || vrange.Max != VisibleRange.Max) {
				VisibleRange = vrange;
				RaisePropertyChanged( PROP_VisibleRange );
			}

			//Calculate magnitudes of visible range and num ticks
			double magDes = Math.Floor( Math.Log10( Math.Abs( _numTicksDesired ) ) );
			double magVis = Math.Floor( Math.Log10( Math.Abs( VisibleRange.Range ) ) );

			//Calculate num ticks
			if (lockNum)
				_numTicks = _numTicksDesired;

			else {
				double r = vrange.Range / Math.Pow( 10, Math.Floor( Math.Log10( vrange.Range ) ) );
				int nr = (int)Math.Round( _numTicksDesired / r );
				switch (nr) {
					case 1: nr = 5; break;
					case 2:
					case 3: nr = 2; break;
					case 4:
					case 5:
					case 6: nr = 5; break;
					case 7:
					case 8:
					case 9: nr = 10; break;
				}
				_numTicks = (int)Math.Round( r ) * nr;
			}

			RaisePropertyChanged( PROP_Ticks );
		}
		//public void PrepTicks() {
		//    //Decode lock flags
		//    bool lockMin = Flags.HasFlag( VisibleScaleFlags.LockBottom );
		//    bool lockMax = Flags.HasFlag( VisibleScaleFlags.LockTop );
		//    bool lockNum = Flags.HasFlag( VisibleScaleFlags.LockNumTicks );

		//    //Decode truncation modes
		//    TruncationMode minRound = Flags.HasFlag( VisibleScaleFlags.ShowAllData ) ? (Domain.Inverted ? TruncationMode.More : TruncationMode.Less) : TruncationMode.Nearest;
		//    TruncationMode maxRound = Flags.HasFlag( VisibleScaleFlags.ShowAllData ) ? (Domain.Inverted ? TruncationMode.Less : TruncationMode.More) : TruncationMode.Nearest;

		//    //Calculate desirable range
		//    int rangeMag = (int)Math.Floor( Math.Log10( Math.Abs( Range.Range ) ) );

		//    //Define visible range (depends on desired range and flags)
		//    Limits vrange = new Limits();
		//    vrange.Min = (lockMin) ? Range.Min : (double)Niceify( Range.Min, 1 - rangeMag, minRound );
		//    vrange.Max = (lockMax) ? Range.Max : (double)Niceify( Range.Max, 1 - rangeMag, maxRound );
		//    VisibleRange = vrange;
		//    RaisePropertyChanged( PROP_VisibleRange );

		//    //Calculate magnitudes of visible range and num ticks
		//    double magDes = Math.Floor( Math.Log10( Math.Abs( _numTicksDesired ) ) );
		//    double magVis = Math.Floor( Math.Log10( Math.Abs( VisibleRange.Range ) ) );

		//    //Calculate num ticks
		//    if (lockNum)
		//        _numTicks = _numTicksDesired;

		//    else {
		//        int num = (int)(VisibleRange.Range * Math.Pow( 10, magDes - magVis ));

		//        if (num > _numTicksDesired) {
		//            int div = (int)Niceify( num / _numTicksDesired, 0 );
		//            if (div != 0) num /= div;
		//        } else {
		//            int div = (int)Niceify( _numTicksDesired / num, 0 );
		//            if (div != 0) num *= div;
		//        }

		//        _numTicks = num;
		//    }

		//    RaisePropertyChanged( PROP_Ticks );
		//}
		public decimal GetTick( double i, out double plotVal ) {
			double val = Domain.Min + i * Domain.Range / NumTicks;
			decimal ret = Math.Round( Niceify( ToDataSpace( val ), _tickSig + ((i == (int)i) ? 0 : 1) ), 2 );
			plotVal = ToPlotSpace( (double)ret );
			return ret;
		}


		public int GetSubdivisions( int desired ) {
			int sig = _tickSig;
			double r = Math.Pow( 10, sig ) * VisibleRange.Range;
			double intv = r / NumTicks;
			return (int)(intv * (int)(desired / intv));
		}
	}
}
